#include <logger.h>
#include <napi_call.h>
#include <node_api.h>

napi_value Throw(napi_env env, napi_callback_info info)
{
    size_t argc = 1;
    napi_value argv0 = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &argv0, nullptr, nullptr));

    if (argc == 1) {
        NAPI_CALL(env, napi_throw(env, argv0));
    }
    return nullptr;
}

bool GetStringValue(napi_env env,
                    napi_callback_info info,
                    int index,
                    std::string &outstr)
{
    size_t argc = index + 1;
    napi_value *argv = new napi_value[argc];
    NAPI_CALL_RET(env, napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr), false);
    if (index >= argc) {
        return true;
    }

    char str[32] = {};
    napi_status ret = napi_get_value_string_utf8(env, argv[index], str, sizeof(str), &argc);
    if (ret != napi_ok) {
        const napi_extended_error_info* einfo = nullptr;
        NAPI_CALL_RET(env, napi_get_last_error_info(env, &einfo), false);
        std::stringstream ss;
        ss << einfo->error_message << " at " << (index + 1);
        switch (index + 1) {
            case 1: ss << "st"; break;
            case 2: ss << "nd"; break;
            case 3: ss << "rd"; break;
            default: ss << "th"; break;
        }
        ss << " parameter";
        NAPI_CALL_RET(env, napi_throw_error(env, nullptr, ss.str().c_str()), false);
        return false;
    }

    outstr = str;
    return true;
}

napi_value ThrowError(napi_env env, napi_callback_info info)
{
    std::string code = "no_code";
    std::string mesg = "no_mesg";
    if (bool ret = GetStringValue(env, info, 0, code); !ret) {
        return nullptr;
    }

    if (bool ret = GetStringValue(env, info, 1, mesg); !ret) {
        return nullptr;
    }

    NAPI_CALL(env, napi_throw_error(env, code.c_str(), mesg.c_str()));
    return nullptr;
}

napi_value ThrowTypeError(napi_env env, napi_callback_info info)
{
    std::string code = "no_code";
    std::string mesg = "no_mesg";
    if (bool ret = GetStringValue(env, info, 0, code); !ret) {
        return nullptr;
    }

    if (bool ret = GetStringValue(env, info, 1, mesg); !ret) {
        return nullptr;
    }

    NAPI_CALL(env, napi_throw_type_error(env, code.c_str(), mesg.c_str()));
    return nullptr;
}

napi_value ThrowRangeError(napi_env env, napi_callback_info info)
{
    std::string code = "no_code";
    std::string mesg = "no_mesg";
    if (bool ret = GetStringValue(env, info, 0, code); !ret) {
        return nullptr;
    }

    if (bool ret = GetStringValue(env, info, 1, mesg); !ret) {
        return nullptr;
    }

    NAPI_CALL(env, napi_throw_range_error(env, code.c_str(), mesg.c_str()));
    return nullptr;
}

napi_value CreateError(napi_env env, napi_callback_info info)
{
    std::string code = "no_code";
    std::string mesg = "no_mesg";
    if (bool ret = GetStringValue(env, info, 0, code); !ret) {
        return nullptr;
    }

    if (bool ret = GetStringValue(env, info, 1, mesg); !ret) {
        return nullptr;
    }

    napi_value codeVal = nullptr;
    napi_value mesgVal = nullptr;
    napi_value error = nullptr;
    NAPI_CALL(env, napi_create_string_utf8(env, code.c_str(), NAPI_AUTO_LENGTH, &codeVal));
    NAPI_CALL(env, napi_create_string_utf8(env, mesg.c_str(), NAPI_AUTO_LENGTH, &mesgVal));
    NAPI_CALL(env, napi_create_error(env, codeVal, mesgVal, &error));
    return error;
}

napi_value CreateTypeError(napi_env env, napi_callback_info info)
{
    std::string code = "no_code";
    std::string mesg = "no_mesg";
    if (bool ret = GetStringValue(env, info, 0, code); !ret) {
        return nullptr;
    }

    if (bool ret = GetStringValue(env, info, 1, mesg); !ret) {
        return nullptr;
    }

    napi_value codeVal = nullptr;
    napi_value mesgVal = nullptr;
    napi_value error = nullptr;
    NAPI_CALL(env, napi_create_string_utf8(env, code.c_str(), NAPI_AUTO_LENGTH, &codeVal));
    NAPI_CALL(env, napi_create_string_utf8(env, mesg.c_str(), NAPI_AUTO_LENGTH, &mesgVal));
    NAPI_CALL(env, napi_create_type_error(env, codeVal, mesgVal, &error));
    return error;
}

napi_value CreateRangeError(napi_env env, napi_callback_info info)
{
    std::string code = "no_code";
    std::string mesg = "no_mesg";
    if (bool ret = GetStringValue(env, info, 0, code); !ret) {
        return nullptr;
    }

    if (bool ret = GetStringValue(env, info, 1, mesg); !ret) {
        return nullptr;
    }

    napi_value codeVal = nullptr;
    napi_value mesgVal = nullptr;
    napi_value error = nullptr;
    NAPI_CALL(env, napi_create_string_utf8(env, code.c_str(), NAPI_AUTO_LENGTH, &codeVal));
    NAPI_CALL(env, napi_create_string_utf8(env, mesg.c_str(), NAPI_AUTO_LENGTH, &mesgVal));
    NAPI_CALL(env, napi_create_range_error(env, codeVal, mesgVal, &error));
    return error;
}

napi_value CatchInnerException(napi_env env, napi_callback_info info)
{
    bool pending = false;
    napi_value error = nullptr;
    bool isError = false;
    NAPI_CALL(env, napi_is_exception_pending(env, &pending));
    VALUE_LOGGER(pending);
    // pending: 'false'

    // throw 0
    napi_value int32 = nullptr;
    NAPI_CALL(env, napi_create_int32(env, 0, &int32));
    NAPI_CALL(env, napi_throw(env, int32));

    // check
    NAPI_CALL(env, napi_is_exception_pending(env, &pending));
    NAPI_CALL(env, napi_get_and_clear_last_exception(env, &error));
    NAPI_CALL(env, napi_is_error(env, error, &isError));
    VALUE_LOGGER(pending);
    VALUE_LOGGER(isError);
    // pending: 'true'
    // isError: 'false'

    // throw Error("code", "mesg")
    NAPI_CALL(env, napi_throw_error(env, "code", "mesg"));

    // check
    NAPI_CALL(env, napi_is_exception_pending(env, &pending));
    NAPI_CALL(env, napi_get_and_clear_last_exception(env, &error));
    NAPI_CALL(env, napi_is_error(env, error, &isError));
    VALUE_LOGGER(pending);
    VALUE_LOGGER(isError);
    // pending: 'true'
    // isError: 'true'

    return nullptr;
}

napi_value FatalError(napi_env env, napi_callback_info info)
{
    /* no return */ napi_fatal_error("location", NAPI_AUTO_LENGTH, "message", NAPI_AUTO_LENGTH);
}

/* napi_value */
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */)
{
    napi_property_descriptor props[] = {
        {.utf8name = "throw", .method = Throw},
        {.utf8name = "throw_error", .method = ThrowError},
        {.utf8name = "throw_type_error", .method = ThrowTypeError},
        {.utf8name = "throw_range_error", .method = ThrowRangeError},
        {.utf8name = "create_error", .method = CreateError},
        {.utf8name = "create_type_error", .method = CreateTypeError},
        {.utf8name = "create_range_error", .method = CreateRangeError},
        {.utf8name = "catch_inner_exception", .method = CatchInnerException},
        {.utf8name = "fatal_error", .method = FatalError},
    };
    for (auto &prop : props) {
        constexpr auto methodAttrs = napi_writable | napi_enumerable | napi_configurable;
        prop.attributes = static_cast<napi_property_attributes>(methodAttrs);
    }
    NAPI_CALL(env, napi_define_properties(env, exports, sizeof(props) / sizeof(*props), props));
    return exports;
}
